﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
// See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Diagnostics.Contracts;

namespace System.Threading.Actors
{
    // Encapsulates the possible statuses associated with an actor method call
    public enum ActorServiceResultStatus
    {
        Succeeded,
        Failed,
        TryAgain, // transient failure, e.g. insufficient quorum
        WrongTarget, // Node has transitioned from primary to some other role
        // Queued -- to be added later when we add support for call batching
    }

    [Serializable]
    public class ActorServiceResult
    {
        public ActorServiceResultStatus Status { get; set; }
        public string Result { get; set; } // json-encoded, only valid if Status==Succeeded
        // This is necessary so that the receiving end can properly downcast longs to ints
        // If we need to, we can change this to a string or enum to allow us to cover [u]byte, [u]short, [u]int, [u]long
        public bool IsIntResult { get; set; } // only valid if Status==Succeeded
        public string ExceptionType { get; set; } // only valid if Status==Failed
        public string ExceptionMessage { get; set; } // only valid if Status==Failed
        public string ExceptionStackTrace { get; set; } // only valid if Status==Failed
        public int ClientSequenceNumber { get; set; } // Provides a reference to the originating request

        // The Json deserializer requires either an empty constructor or one marked "JsonConstructor"
        public ActorServiceResult()
        {
        }

        // Using factory methods instead of constructors because the constructor that just took a result would
        // have potentially conflicted with the constructor that just took an exception (if a result could be
        // of type Exception).

        // Build an ActorServiceResult from a successfully-obtained method result
        public static ActorServiceResult FromResult(int clientSequenceNumber, object result)
        {
            ActorServiceResult asr = new ActorServiceResult();
            asr.Status = ActorServiceResultStatus.Succeeded;
            asr.Result = JsonConvert.SerializeObject(result);
            asr.IsIntResult = (result is int);
            asr.ClientSequenceNumber = clientSequenceNumber;
            return asr;
        }

        // Build an ActorServiceResult from an exception
        public static ActorServiceResult FromException(int clientSequenceNumber, Exception e)
        {
            var baseException = (e is AggregateException) ? ((AggregateException)e).GetBaseException() : e;
            var asr = ActorServiceResult.FromException(clientSequenceNumber, baseException.GetType().Name, baseException.Message, baseException.StackTrace);
            return asr;
        }

        // Build an ActorServiceResult from exception information
        // This longer form allows one to expand the message of an original exception, or to create a faulted ActorMethodResult
        // without an actual exception in hand.
        public static ActorServiceResult FromException(int clientSequenceNumber, string exceptionType, string exceptionMessage, string exceptionStackTrace)
        {
            ActorServiceResult asr = new ActorServiceResult();
            if (exceptionType.Equals("FabricTransientException"))
                asr.Status = ActorServiceResultStatus.TryAgain;
            else if (exceptionType.Equals("FabricNotPrimaryException"))
                asr.Status = ActorServiceResultStatus.WrongTarget;
            else
                asr.Status = ActorServiceResultStatus.Failed;

            asr.ExceptionType = exceptionType;
            asr.ExceptionMessage = exceptionMessage;
            asr.ExceptionStackTrace = exceptionStackTrace;
            asr.ClientSequenceNumber = clientSequenceNumber;
            return asr;
        }

        public T GetTypedResult<T>()
        {
            return JsonConvert.DeserializeObject<T>(Result);
        }


        // Provide a fast JSON serializer for ActorServiceResult
        public string ToJson()
        {
            var sw = new StringWriter();
            var writer = new JsonTextWriter(sw);

            writer.WriteStartObject();
            writer.WritePropertyName("Status"); writer.WriteValue((int)Status);
            if (Result != null)
            {
                writer.WritePropertyName("Result"); writer.WriteValue(Result);
            }
            writer.WritePropertyName("IsIntResult"); writer.WriteValue(IsIntResult);
            if (ExceptionType != null)
            {
                writer.WritePropertyName("ExceptionType"); writer.WriteValue(ExceptionType);
            }
            if (ExceptionMessage != null)
            {
                writer.WritePropertyName("ExceptionMessage"); writer.WriteValue(ExceptionMessage);
            }
            if (ExceptionStackTrace != null)
            {
                writer.WritePropertyName("ExceptionStackTrace"); writer.WriteValue(ExceptionStackTrace);
            }
            writer.WritePropertyName("ClientSequenceNumber"); writer.WriteValue(ClientSequenceNumber);
            writer.WriteEndObject();

            return sw.ToString();
        }

        // Provide a fast JSON deserializer for ActorServiceResult
        public static ActorServiceResult FromJson(string json)
        {
            Contract.Requires(json != null);

            var sr = new StringReader(json);
            var reader = new JsonTextReader(sr);
            var asr = new ActorServiceResult();
            while (reader.Read())
            {
                if (reader.TokenType == JsonToken.PropertyName)
                {
                    var name = (string)reader.Value;
                    if (name.Equals("Status"))
                    {
                        reader.Read();
                        var value = (ActorServiceResultStatus)(long)reader.Value;
                        Contract.Assume(Enum.IsDefined(typeof(ActorServiceResultStatus), value), "Undefined ActorServiceResultStatus value.");
                        asr.Status = value;
                    }
                    else if (name.Equals("Result"))
                    {
                        reader.Read();
                        asr.Result = (string)reader.Value;
                    }
                    else if (name.Equals("ClientSequenceNumber"))
                    {
                        reader.Read();
                        asr.ClientSequenceNumber = (int)(long)reader.Value;
                    }
                    else if (name.Equals("IsIntResult"))
                    {
                        reader.Read();
                        asr.IsIntResult = (bool)reader.Value;
                    }
                    else if (name.Equals("ExceptionType"))
                    {
                        reader.Read();
                        asr.ExceptionType = (string)reader.Value;
                    }
                    else if (name.Equals("ExceptionMessage"))
                    {
                        reader.Read();
                        asr.ExceptionMessage = (string)reader.Value;
                    }
                    else if (name.Equals("ExceptionStackTrace"))
                    {
                        reader.Read();
                        asr.ExceptionStackTrace = (string)reader.Value;
                    }
                }
            }

            return asr;

        }

        public Exception ConstructException()
        {
            Contract.Requires(Status == ActorServiceResultStatus.Failed);

            return ActorClient.ConstructException(this);
        }
    }
}
